Understanding async and await, Task.WaitAll, Task.Run, and parallelism
Ok. You are probably new to async and await or maybe you aren’t new but you’ve never deep dived into it. You may not understand some simple truths:
- aync/await does NOT give you parallelism for free.
- Tasks are not necessary parallel. They can be if you code them to be.
- The recommendation “You should always use await” is not really true when you want parallelism, but is still sort of true.
- Task.WhenAll is both parallel and async.
- Task.WaitAll only parallel.
Here is a sample project that will help you learn.
There is more to learn in the comments.
There is more to learn by running this.
Note: I used Visual Studio 2017 and compiled with .Net 7.1, which required that I go to the project properties | Build | Advanced | Language Version and set the language to C# 7.1 or C# latest minor version.
using System; using System.Diagnostics; using System.Threading.Tasks; namespace MultipleAwaitsExample { class Program { static async Task Main(string[] args) { Console.WriteLine("Running with await"); await RunTasksAwait(); Console.WriteLine("Running with Task.WaitAll()"); await RunTasksWaitAll(); Console.WriteLine("Running with Task.WhenAll()"); await RunTasksWhenAll(); Console.WriteLine("Running with Task.Run()"); await RunTasksWithTaskRun(); Console.WriteLine("Running with Parallel"); RunTasksWithParallel(); } /// <summary> /// Pros: It works /// Cons: The tasks are NOT run in parallel. /// Code after the await is not run while the await is awaited /// **If you want parallelism, this isn't even an option.** /// Slowest. Because of no parallelism. /// </summary> public static async Task RunTasksAwait() { var group = "await"; Stopwatch watcher = new Stopwatch(); watcher.Start(); await MyTaskAsync(1, 500, group); await MyTaskAsync(2, 300, group); await MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// WaitAll behaves quite differently from WhenAll /// Pros: It works /// The tasks run in parallel /// Cons: It isn't clear whether the code is parallel here, but it is. /// It isn't clear whether the code is async here, but it is NOT. /// There is a Visual Studio usage warning. You can remove async to get rid of it because it isn't an Async method. /// The return value is wrapped the Result property of the task /// Breaks Aync end-to-end /// Note: I can't foresee usecase where WaitAll would be preferred over WhenAll. /// </summary> public static async Task RunTasksWaitAll() { var group = "WaitAll"; Stopwatch watcher = new Stopwatch(); watcher.Start(); var task1 = MyTaskAsync(1, 500, group); var task2 = MyTaskAsync(2, 300, group); var task3 = MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); Task.WaitAll(task1, task2, task3); watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// WhenAll gives you the best of all worlds. The code is both parallel and async. /// Pros: It works /// The tasks run in parallel /// Code after the tasks run while the task is running /// Doesn't break end-to-end async /// Cons: It isn't clear you are doing parallelism here, but you are. /// There is a Visual Studio usage warning /// The return value is wrapped the Result property of the task /// </summary> public static async Task RunTasksWhenAll() { var group = "WaitAll"; Stopwatch watcher = new Stopwatch(); watcher.Start(); var task1 = MyTaskAsync(1, 500, group); // You can't use await if you want parallelism var task2 = MyTaskAsync(2, 300, group); var task3 = MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); await Task.WhenAll(task1, task2, task3); // But now you are calling await, so you are sort of still awaiting watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// Pros: It works /// The tasks run in parrallel /// Code can run immediately after the tasks but before the tasks complete /// Allows for running non-async code asynchonously /// Cons: It isn't clear whether the code is doing parallelism here. It isn't. /// The lambda syntax affects readability /// Breaks Aync end-to-end /// </summary> public static async Task RunTasksWithTaskRun() { var group = "Task.Run()"; Stopwatch watcher = new Stopwatch(); watcher.Start(); await Task.Run(() => MyTask(1, 500, group)); await Task.Run(() => MyTask(2, 300, group)); await Task.Run(() => MyTask(3, 100, group)); Console.WriteLine("Code immediately after tasks."); watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// Pros: It works /// It is clear in the code you want to run these tasks in parallel. /// Code can run immediately after the tasks but before the tasks complete /// Fastest /// Cons: There is no async or await. /// Breaks Aync end-to-end. You can workaround this by wrapping Parallel.Invoke in a Task.Run method. See commented code. /// </summary> public /* async */ static void RunTasksWithParallel() { var group = "Parallel"; Stopwatch watcher = new Stopwatch(); watcher.Start(); //await Task.Run(() => Parallel.Invoke( () => MyTask(1, 500, group), () => MyTask(2, 300, group), () => MyTask(3, 100, group), () => Console.WriteLine("Code immediately after tasks.") ); //); watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } public static async Task MyTaskAsync(int i, int milliseconds, string group) { await Task.Delay(milliseconds); Console.WriteLine($"{group}: {i}"); } public static void MyTask(int i, int milliseconds, string group) { var task = Task.Delay(milliseconds); task.Wait(); Console.WriteLine($"{group}: {i}"); } } }
And here is the same example but this time with some return values.
using System; using System.Diagnostics; using System.Threading.Tasks; namespace MultipleAwaitsExample { class Program1 { static async Task Main(string[] args) { Console.WriteLine("Running with await"); await RunTasksAwait(); Console.WriteLine("Running with Task.WaitAll()"); await RunTasksWaitAll(); Console.WriteLine("Running with Task.WhenAll()"); await RunTasksWhenAll(); Console.WriteLine("Running with Task.Run()"); await RunTasksWithTaskRun(); Console.WriteLine("Running with Parallel"); RunTasksWithParallel(); } /// <summary> /// Pros: It works /// Cons: The tasks are NOT run in parallel. /// Code after the await is not run while the await is awaited /// **If you want parallelism, this isn't even an option.** /// Slowest. Because of no parallelism. /// </summary> public static async Task RunTasksAwait() { var group = "await"; Stopwatch watcher = new Stopwatch(); watcher.Start(); // You just asign the return variables as normal. int result1 = await MyTaskAsync(1, 500, group); int result2 = await MyTaskAsync(2, 300, group); int result3 = await MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); watcher.Stop(); // You now have access to the return objects directly. Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// WaitAll behaves quite differently from WhenAll /// Pros: It works /// The tasks run in parallel /// Cons: It isn't clear whether the code is parallel here, but it is. /// It isn't clear whether the code is async here, but it is NOT. /// There is a Visual Studio usage warning. You can remove async to get rid of it because it isn't an Async method. /// The return value is wrapped the Result property of the task /// Breaks Aync end-to-end /// Note: I can't foresee usecase where WaitAll would be preferred over WhenAll. /// </summary> public static async Task RunTasksWaitAll() { var group = "WaitAll"; Stopwatch watcher = new Stopwatch(); watcher.Start(); var task1 = MyTaskAsync(1, 500, group); var task2 = MyTaskAsync(2, 300, group); var task3 = MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); Task.WaitAll(task1, task2, task3); watcher.Stop(); // You now have access to the return object using the Result property. int result1 = task1.Result; int result2 = task2.Result; int result3 = task3.Result; Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// WhenAll gives you the best of all worlds. The code is both parallel and async. /// Pros: It works /// The tasks run in parallel /// Code after the tasks run while the task is running /// Doesn't break end-to-end async /// Cons: It isn't clear you are doing parallelism here, but you are. /// There is a Visual Studio usage warning /// The return value is wrapped the Result property of the task /// </summary> public static async Task RunTasksWhenAll() { var group = "WaitAll"; Stopwatch watcher = new Stopwatch(); watcher.Start(); var task1 = MyTaskAsync(1, 500, group); // You can't use await if you want parallelism var task2 = MyTaskAsync(2, 300, group); var task3 = MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); await Task.WhenAll(task1, task2, task3); // But now you are calling await, so you are sort of still awaiting watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// Pros: It works /// The tasks run in parrallel /// Code can run immediately after the tasks but before the tasks complete /// Allows for running non-async code asynchonously /// Cons: It isn't clear whether the code is doing parallelism here. It isn't. /// The lambda syntax affects readability /// Breaks Aync end-to-end /// </summary> public static async Task RunTasksWithTaskRun() { var group = "Task.Run()"; Stopwatch watcher = new Stopwatch(); watcher.Start(); int result1 = await Task.Run(() => MyTask(1, 500, group)); int result2 = await Task.Run(() => MyTask(2, 300, group)); int result3 = await Task.Run(() => MyTask(3, 100, group)); Console.WriteLine("Code immediately after tasks."); watcher.Stop(); // You now have access to the return objects directly. Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// Pros: It works /// It is clear in the code you want to run these tasks in parallel. /// Code can run immediately after the tasks but before the tasks complete /// Fastest /// Cons: There is no async or await. /// Breaks Aync end-to-end. You can workaround this by wrapping Parallel.Invoke in a Task.Run method. See commented code. /// </summary> public /* async */ static void RunTasksWithParallel() { var group = "Parallel"; Stopwatch watcher = new Stopwatch(); watcher.Start(); // You have to declare your return objects before hand. //await Task.Run(() => int result1, result2, result3; Parallel.Invoke( () => result1 = MyTask(1, 500, group), () => result2 = MyTask(2, 300, group), () => result3 = MyTask(3, 100, group), () => Console.WriteLine("Code immediately after tasks.") ); //); // You now have access to the return objects directly. watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } public static async Task<int> MyTaskAsync(int i, int milliseconds, string group) { await Task.Delay(milliseconds); Console.WriteLine($"{group}: {i}"); return i; } public static int MyTask(int i, int milliseconds, string group) { var task = Task.Delay(milliseconds); task.Wait(); Console.WriteLine($"{group}: {i}"); return i; } } }